Charlie Calvert's C++ Builder Unleashed
- 26 -
Extending an Internet
Server with ISAPI
Overview
This chapter examines the ISAPI and CGI technologies. You can use CGI to create
applications that extend a Web server, and you can use ISAPI to create DLLs that
extend a Web server. In particular, ISAPI allows you to write scripts and filters
and to interact dynamically with a user of your browser.
ISAPI technology is specific to the Internet Information Server that ships with
Windows NT and to the Personal Web Server that ships with Microsoft FrontPage. It
is, however, merely a specification, and other servers could conform to it if they
wish. Several different vendors, including Borland, have created technology that
allows ISAPI-based technology to be used in conjunction with NSAPI, which is a similar
technology to ISAPI. (The NSAPI/ISAPI bridge is available in Delphi 3.0, for example.)
ISAPI programming is very similar to CGI programming. The only major difference
is that you're creating a DLL instead of an executable. DLLs are advantageous because
they can be loaded into the address space of the Web server. This capability gives
them a leg up over CGI when you're considering performance. CGI, on the other hand,
is a very simple specification to use, and it adapts itself easily to database applications.
ISAPI's reliance on the Windows platform might be a serious limitation in some
other context, but because C++Builder also relies on Windows, discussing the topic
at length in this book makes sense. Another feature to recommend ISAPI is its extreme
simplicity. Friendly, powerful, easy-to-use APIs are the bread and butter of this
book, and ISAPI fits the bill beautifully. Writing BCB database applications with
ISAPI is, however, a bit tricky.
The first half of this chapter deals with ISAPI, and the second half deals with
CGI. I will show how to retrieve data from both ISAPI DLLs and CGI applications,
though my treatment of the subject is more complete in the section on CGI. If you
are interested primarily in ISAPI, you should also take the time to read about the
CGI database applications in the second half of this chapter.
The C++ code in this chapter relies on the presence of several HTML files that
are quoted in full in this chapter and that are also available on the CD that ships
with this book. However, the C++ code will not function properly unless the HTML
files quoted in this chapter are in the correct location on your hard drive. See
the README.TXT file that comes with the CD for additional information on
setting up your system correctly to run this code. Some of these files are located
in the root of the Chap26 directory on the CD. You will also need to use
several databases' aliases described in the readme file.
Hardware and Software Requirements
As I implied in the "Overview" section for this chapter, the code discussed
here requires one of the following:
- A copy of Microsoft Windows NT 3.51 Server or NT 4.0 or later server with the
Internet Information Server loaded
- Windows 95 and a copy of the Personal Web Server that ships with FrontPage
At the time of this writing, it is not clear whether the Personal Web Server (PWS)
will be available from other sources besides FrontPage. I downloaded the beta copy
I used while writing this book directly from Microsoft's Web site. Checking to see
if this copy is still available in that form is probably worthwhile.
You should also check to see if the Netscape or WebSite servers are now supporting
the ISAPI API. An unfortunate and rather unseemly economic battle between Microsoft
and its various competitors may slow down or even halt the spread of ISAPI as a standard,
but checking to see if the battle has cooled somewhat is still worthwhile.
At any rate, the PWS is a useful piece of software for home users to explore.
It will turn any Windows 95 machine into a Web server. I'm not sure how robust it
will be under the strain of more than a few contiguous users. However, it is ideal
if you want to set up a Web server in your home or in a small office. WebSite, from
O'Reilly, is another fine product to turn to, particularly if you want to select
a well-tested, robust server that can carry a heavy load.
You should also have a second computer equipped with a Web browser. This second
computer can be running any operating system and can use virtually any software that
supports Web browsing. I can think of no reason why you can't test most of this code
on a single machine running a server, of course, but you will hardly get into the
spirit of this enterprise if you're limited to that kind of setup.
I assume that most readers working in a business setting will have an intranet
setup that will allow them to experiment with this technology. If you're working
at home, I cannot stress too often the incredible value of setting up a network in
your house. Network cards are very inexpensive these days. One of the ones I use
in my home cost about $30 new. Network cable is also inexpensive, and both Windows
95 and Windows NT come equipped with all the software you need to set up a network
that supports both file browsing with Windows Explorer and also TCP/IP.
When I first set up a network in my home, I thought I was pushing the extreme
edge of modern technology. Now I simply take it for granted and can't understand
how in the world I ever got along without it. Old computers don't have to die; they
can just become Web servers. Small hard drives are extended easily by sharing storage
space across multiple machines. After all, you don't need a separate copy of every
application or every file on each machine. You can just share drives back and forth
between machines, thereby saving a tremendous amount of space.
Most importantly, you can study and experiment with your network at your leisure
and then apply that knowledge at work. A home network is an ideal place to educate
yourself regarding this valuable technology.
Getting More Information on ISAPI
The best place to go for information on ISAPI is the Microsoft MSDN or the Microsoft
Internet SDK. These two sources provide most of the information you need that can't
be found in this book.
Here's a place you can go on the Web if you want to find out more about the ISAPI
specification:
http://www.microsoft.com/win32dev/apiext/isalegal.htm
Of course, I can't guarantee that this Web page will still be in existence when
you read this book. However, two relatively stable sites on the Web that should serve
as links to this spot are
http://www.microsoft.com/intdev/
http://www.microsoft.com/win32dev/
Check in at both these sites on fairly regular intervals to get updates on Win32
and Internet technology.
ISAPI
As stated in the Microsoft documentation, ISAPI allows you to "Write server-side
scripts and filters to extend the capabilities of Microsoft Internet Information
Server and other ISAPI Web servers."
ISAPI is a very easy-to-use yet extremely powerful technology that allows you
to extend the reach of the Internet Information Server or the Personal Web Server.
This tool allows you to make your Web site do pretty much whatever you want it to
do. For example, it provides a means for you to
- Set up interactive responses to user input
- Provide database browsing and updating
- Filter input to your browser to track who signs on and where he or she goes
In the past, the best way to extend a Web server was to create CGI applications.
These powerful tools were limited by their executable format. When you sent in a
CGI-based request from a browser to a server, the CGI application in question usually
had to be loaded into memory, which took a long time. Also, the CGI technology could
be a bit awkward to use under some circumstances.
ISAPI is a method of writing DLLs that replace CGI applications. You can also
write filters with ISAPI, though this subject is not covered in this book. ISAPI
has the advantage of being easier to use than CGI, plus it can be much faster and
make much better use of system resources. In particular, the following points help
explain why ISAPI DLLs are better than CGI applications:
- ISAPI DLLs live in the same address space as the HTTP server. They can therefore
directly access the HTTP services available from the server. They load into memory
more quickly and have much less overhead when it comes to making a call from the
server. These capabilities can be particularly helpful if you're working under a
heavy load.
- You can control when the DLL is loaded or unloaded. For example, you can preload
DLLs for fast access on the first try or unload the ISAPI applications DLLs that
are not being used to free system resources. You can do the same thing with the CGI
executable, but the executable format was not really designed for this kind of manipulation,
although this is part of the native capability of DLLs.
In this chapter, I will concentrate on writing DLLs that return datasets or that
simply communicate with the user who is running a browser. I will not explore filters
at all. For information on filters, you should go to the Microsoft Web site or browse
the MSDN.
ISAPI Basics
The file Httpext.h contains the key declarations used with ISAPI. This
file should ship with C++Builder and is available with versions of the Microsoft
SDK dated later than July 1996. It should also appear in the \include\vcl
directory as ISAPI.HPP. Because it is a Windows 95- or Windows NT-based
technology, you must be using a 32-bit compiler to access this technology. You can't
use it from a 16-bit compiler, nor is it available on Windows 3.1.
Httpext.h contains the interface to the ISAPI technology created by Microsoft.
At the time of this writing, C++Builder has no custom interface for ISAPI, and I
will describe only how to use Microsoft's existing technology. However, ISAPI is
extremely easy to use, and the addition of a custom object is not necessary for most
users.
Three functions can serve as entry points to ISAPI DLLs. The first two listed
here are mandatory, whereas the third is optional:
- GetExtensionVersion: This function just does minimal version checking.
- HttpExtensionProc: This function is the entry point of the DLL, like
the main begin..end block in a Delphi application.
- TerminateExtension: This optional routine can be used to clean up threads
of other memory allocations.
When you're creating an ISAPI DLL, you must export the first two of the three
preceding functions. Implementing these two functions is the key to all ISAPI programming.
In C++Builder, DEF files are frowned upon. You should therefore make sure that
Httpext.h has been modified to export both of these functions with __declspec(dllexport):
BOOL WINAPI __declspec(dllexport) GetExtensionVersion(HSE_VERSION_INFO *pVer);
DWORD
WINAPI
__declspec(dllexport) HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);
TerminateExtension is new in ISAPI 2.0. Its declaration looks like this:
BOOL WINAPI TerminateExtension( DWORD dwFlags );
TerminateExtension is called just before a connection is broken. It provides
a place for you to deallocate memory allocated inside your DLL.
These three routines all contain the word Extension. This term is used
because ISAPI DLLs extend the Internet Information Server or the Personal Web Server.
(Remember, the Internet Information Server is Microsoft's Web server. If you want
to turn an NT Server into a Web server, you use this tool. It ships with NT 4.0 and
is installed automatically during the setup of that operating system.)
Using GetExtensionVersion
The GetExtensionVersion function must be exported from your DLL; otherwise,
the server will not load your DLL. The only job of this function is to report the
version of ISAPI that you expect to support.
You can always just cut and paste the GetExtensionVersion code into your
DLLs. You need to change the function only slightly when you want to change the description
passed in the lpszExtensionDesc field of the HSE_VERSION_INFO struct:
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL");
return (TRUE);
};
The parameter passed to this function is declared in Httpext.h as follows:
typedef struct _HSE_VERSION_INFO {
DWORD dwExtensionVersion; // Version info
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; // Description
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
The two fields of the record are self-explanatory, with the first containing the
ISAPI version number and the second holding a user-defined string describing the
purpose of the DLL. The following are some constants declared in the DLL that are
used in the preceding code:
#define HSE_MAX_EXT_DLL_NAME_LEN 256
#define HSE_VERSION_MAJOR 1 // major version of this spec
#define HSE_VERSION_MINOR 0 // minor version of this spec
That's all you need to do to set up the first of the two mandatory functions in
an ISAPI DLL. The next step, using HttpExtensionProc, is a bit more complex,
so I will treat it in its own section.
Working with the HttpExtensionProc
The HttpExtensionProc routine is the entry point for the DLL. It serves
the same purpose that the main() routine does in a C program, or that the
main begin..end pair does in a Delphi program.
Here is a very simple example of an HttpExtensionProc routine:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
char ResultString[500];
DWORD resultLen;
char *IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL";
strcpy(pECB->lpszLogData, IsapiLogText);
pECB->dwHttpStatusCode = 200;
char *HtmlInfo =
"<HTML>"
"<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>"
"<H1>ISAPI1 Test Results</H1>"
"<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">"
"Hello from a C++ Builder ISAPI DLL!<BR></BODY>"
"</HTML>";
sprintf(ResultString,
"HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s", 500, HtmlInfo);
resultLen = lstrlen(ResultString);
fprintf(out, ResultString);
pECB->WriteClient(pECB->ConnID, ResultString, &resultLen, 0);
return (HSE_STATUS_SUCCESS);
}
If you queried a DLL containing this function from a browser, you would get a
page back with this message:
ISAPI1 Test Results
Hello from a C++ Builder ISAPI DLL!
In the next few paragraphs, I will describe the key points of the code shown here.
However, I'll help you develop a complete understanding of this routine slowly in
the next few sections of the chapter.
The HTML code for querying the DLL might look something like this:
<a href="/scripts/isapi1.dll">ISAPI1 Example</a>
Most of the body of the function is taken up with simple HTML code that provides
basic information to the user:
char *HtmlInfo =
"<HTML>"
"<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>"
"<H1>ISAPI1 Test Results</H1>"
"<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">"
"Hello from a C++ Builder ISAPI DLL!<BR></BODY>"
"</HTML>";
You also need to fill in a few fields of the EXTENSION_CONTROL_BLOCK:
char *IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL";
strcpy(pECB->lpszLogData, IsapiLogText);
pECB->dwHttpStatusCode = 200;
The lpszLogData field contains the string that will be written to the
log on your server. With the Personal Web Server, this log is kept by default in
the Windows directory, though you can change this in the Administration
section of the server applet found in the Control Panel.
The status code in this example is set to 200, which means "OK."
Other possible values include the following:
HTTP_STATUS_BAD_REQUEST
HTTP_STATUS_AUTH_REQUIRED
HTTP_STATUS_FORBIDDEN
HTTP_STATUS_NOT_FOUND
HTTP_STATUS_SERVER_ERROR
HTTP_STATUS_NOT_IMPLEMENTED
More information on the EXTENSION_CONTROL_BLOCK is provided in the section
called "Working with the EXTENSION_CONTROL_BLOCK."
Notice the function pointer called WriteClient in the struct. You can
call this function to send information back to the browser. When calling this function,
you use the value in the ConnID field of the EXTENSION_CONTROL_BLOCK
struct. ConnID is filled in for you automatically when the HttpExtensionProc
function is called.
Before you look at the EXTENSION_CONTROL_BLOCK struct, let me show you
a complete ISAPI DLL that uses the HttpExtensionProc function shown in this
section.
A Stripped-Down ISAPI Example
The source code in Listing 26.1 shows how to create the simplest possible ISAPI
DLL. The goal is to remove all the complications from the code, and just include
enough information to make sure everything is working correctly.
Listing 26.1. The ISAPI1 example.
///////////////////////////////////////
// FILE: ISAPI1.CPP
// PROJECT: ISAPI1.DLL
// copyright (3) 1996 by Charlie Calvert
//
// This example shows how to use ISAPI, which is similar to creating a
// CGI application. The code should return a simple string to an HTML
// browser such as the Internet Explorer.
//
// Here is the HTML you output in a browser to call this ISAPI DLL:
//
// <HTML>
// <HEAD>
// <TITLE>CharlieC Home Page</TITLE>
// </HEAD>
// <BODY>
// <H1>My Home Page </H1>
// <P>
// This is the home page for my home computer.
// <P>
// <A HREF="/scripts/isapi1.dll" >ISAPI One</A><BR>
// </BODY>
// </HTML>
#include <vcl\vcl.h>
#include <string.h>
#include <stdio.h>
#pragma hdrstop
#include "..\..\utils\Httpext.h"
USERES("Isapi1.res");
FILE *out;
// GetExtensionVersion callback definition
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
fputs("Version", out);
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL");
return (TRUE);
};
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
AnsiString ResultString;
DWORD resultLen;
AnsiString IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL";
strcpy(pECB->lpszLogData, IsapiLogText.c_str());
AnsiString HtmlInfo =
"<HTML>"
"<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>"
"<H1>ISAPI1 Test Results</H1>"
"<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">"
"You are talking to a C++Builder ISAPI DLL."
"<BR></BODY>"
"</HTML>";
pECB->dwHttpStatusCode = 200;
ResultString = Format(
"HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s",
OPENARRAY(TVarRec, (HtmlInfo.Length(), HtmlInfo)));
resultLen = ResultString.Length();
fprintf(out, ResultString.c_str());
pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &resultLen, 0);
return (HSE_STATUS_SUCCESS);
}
#pragma argsused
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
switch (reason)
{
case DLL_PROCESS_ATTACH:
out = fopen("c:\\test.txt", "w+");
fprintf(out,"hello");
break;
case DLL_PROCESS_DETACH:
fprintf(out,"goodbye");
fclose(out);
break;
default:
break;
}
return (TRUE);
}
To use this DLL, you should copy it into a subdirectory of the scripts
directory beneath the root for your Web. On my NT 4.0 machine, the subdirectory looks
like this:
c:\winnt\system32\inetsrv\scripts\mystuff\isapi1.dll
In this case, I have created the directory called MYSTUFF, and it is
used solely for storing ISAPI DLLs I have created. Your mileage may, of course, differ
on your machine, depending on where you put the InetSrv directory and various
other factors.
To call this DLL, you should add the following hyperlink to one of your HTML pages:
<A HREF="/scripts/mystuff/isapi1.dll" >ISAPI One</A><BR>
For example, here is a complete sample page:
<HTML>
<HEAD><TITLE>An ISAPI Page</TITLE></HEAD>
<BODY>
<H1>My ISAPI Page</H1>
<P>This is the home page for ISAPI on my computer.<P>
<A HREF="/scripts/mystuff/isapi1.dll" >ISAPI One</A><BR>
</BODY>
</HTML>
When the user clicks the hyperlink, the ISAPI1 DLL will be called and the string
"Hello from C++ Builder" will appear in the user's browser. If
you did not put the ISAPI1.DLL in the MYSTUFF directory, then you
should change the preceding HTML code to reflect that fact. Notice that the path
you assign is relative to the InetSrv directory and does not, and should
not, contain the entire path to your DLL.
Note that if you copy the ISAPI1.DLL into the MYSTUFF directory
multiple times, you will need to shut down the WWW portion of the Internet server
before each copy. The rule is that you can copy the DLL the first time for free,
but after you have used it, it belongs to the server, and you need to shut down the
WWW services on the server before you can copy an updated version of the file over
the first copy. You can use the Internet Service Manager application to shut down
the WWW services on the NT Server. This application should be in the Microsoft Internet
Server group created in Windows Explorer or Program Manager (NT 3.51) at the time
of the installation of the Internet Information Server. You can use the PWS applet
in the Control Panel if you're using the Personal Web Server on Windows 95 or on
the Windows NT Workstation.
Working with the EXTENSION_CONTROL_BLOCK
By this point in the chapter, you should be able to create your first ISAPI DLL
and call it from a Web browser on a second machine. The rest of this chapter explores
ISAPI in more depth.
The following fairly complex record is passed as the sole parameter to HttpExtensionProc:
typedef struct _EXTENSION_CONTROL_BLOCK {
DWORD cbSize; // size of this struct.
DWORD dwVersion; // version info of this spec
HCONN ConnID; // Context number not to be modified!
DWORD dwHttpStatusCode; // HTTP Status code
CHAR lpszLogData[HSE_LOG_BUFFER_LEN];// log info
LPSTR lpszMethod; // REQUEST_METHOD
LPSTR lpszQueryString; // QUERY_STRING
LPSTR lpszPathInfo; // PATH_INFO
LPSTR lpszPathTranslated; // PATH_TRANSLATED
DWORD cbTotalBytes; // Total bytes indicated from client
DWORD cbAvailable; // Available number of bytes
LPBYTE lpbData; // pointer to cbAvailable bytes
LPSTR lpszContentType; // Content type of client data
BOOL (WINAPI * GetServerVariable) ( HCONN hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSize );
BOOL (WINAPI * WriteClient) ( HCONN ConnID,
LPVOID Buffer,
LPDWORD lpdwBytes,
DWORD dwReserved );
BOOL (WINAPI * ReadClient) ( HCONN ConnID,
LPVOID lpvBuffer,
LPDWORD lpdwSize );
BOOL (WINAPI * ServerSupportFunction)( HCONN hConn,
DWORD dwHSERRequest,
LPVOID lpvBuffer,
LPDWORD lpdwSize,
LPDWORD lpdwDataType );
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
Notice that this record contains the ConnID field referenced previously
and passed as the first parameter to WriteClient.
The first parameter of this record is used for version control. It should be set
to the size of the EXTENSION_CONTROL_BLOCK. If Microsoft changes this structure,
then they can tell which version of the structure they are dealing with by checking
the size of the record as recorded in this field. You should never change any of
the first three fields of this record; these fields are filled out ahead of time
by ISAPI and can only be referenced, not changed, by your program.
The most important field of this record is probably the lpszQueryString,
which contains information about the query passed in from the server. For example,
suppose you have created a DLL called ISAPI1.DLL. To call this DLL, you
would create an HREF that looks like this in one of your browser pages:
<A HREF="/scripts/mystuff/test1.dll">Test One</A>
If you want to query the DLL, you would edit the preceding line so that it looks
like this:
<A HREF="/scripts/mystuff/test1.dll?MyQuery">Test One</A>
Given the second of the two HTML fragments listed here, your DLL would get called
with the string "MyQuery" in the lpszQueryString parameter.
Notice in particular the use of the question mark, followed by the query string itself.
You could, of course, change the query string at will. For example, you could
write
<A HREF="/scripts/mystuff/test1.dll?ServerName">Test One</A>
To this query, the DLL might reply with the name of the server. You have no limits
on what you can pass in this parameter, but the string after the question mark cannot
have any spaces in it. If you need to use spaces, replace them with a plus sign:
Instead of "Server Name", write "Server+Name".
The string can be anything you want, and it is up to you to parse the information
from inside the DLL as you like.
When you return information from the server back to the browser, you use the WriteClient
function pointer that is part of this record.
Writers of CGI applications will notice that the syntax for sending query strings
is familiar. Indeed, ISAPI follows many of the conventions of CGI, and most of the
fields in the EXTENSION_CONTROL_BLOCK are simply borrowed directly to initialize
this pointer; it is passed to you gratis by the Internet Information Server.
Another key field in the EXTENSION_CONTROL_BLOCK is the lpbData
field, which contains any additional information sent to you by the browser. In particular,
it is used to pass information associated with a Submit button. For example, if you
have an HTML form with a number of fields in it, the information from these fields
will be sent in the pointer called lpbData after the Submit button is clicked.
The section of this chapter called "Getting Information from a Submit Button"
focuses on how to handle this situation.
So far I have zeroed in on three key fields of the EXTENSION_CONTROL_BLOCK:
- WriteClient: A pointer to a function that allows you to send formatted
HTML data back to the browser. This function uses the ConnID field of EXTENSION_CONTROL_BLOCK.
- lpszQueryString: The query passed to you from the browser.
- lpbData: Any additional data being passed to you from the browser. This
data is usually the contents of any fields on an HTML form. I discuss this field
further in the section on the Submit button.
Mirroring the Fields of the EXTENSION_CONTROL_BLOCK
The best way to get a feeling for how the rest of the fields of EXTENSION_CONTROL_BLOCK
work is simply to mirror them back to yourself in a browser. In other words, you
can create an HTML page that allows the user to call a custom ISAPI DLL. The purpose
of this ISAPI DLL is simply to snag the contents of each field of the EXTENSION_CONTROL_BLOCK,
format them in HTML, and send them back to the browser. This will turn your browser
into a rather jazzy debugger that shows each of the fields in the EXTENSION_CONTROL_BLOCK.
Listing 26.2 contains the source to a DLL called IsapiVars that performs
this task.
Listing 26.2. The IsapiVars code
that mirrors back the parameters sent in the EXTENSION_CONTROL_BLOCK.
///////////////////////////////////////
// IsapiVars.cpp
// Mirror back the information sent to an ISAPI DLL by the server
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "..\..\utils\Httpext.h"
USERES("IsapiVars.res");
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL");
return (TRUE);
};
#define SIZE 2048
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
char ResultString[SIZE * 2];
char HtmlInfo[SIZE];
char Buffer[SIZE];
DWORD StrSize;
DWORD resultLen;
char *IsapiLogText = "ISAPIVars from C++ Builder";
strcpy(pECB->lpszLogData, IsapiLogText);
pECB->dwHttpStatusCode = 200;
sprintf(HtmlInfo,
"<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>"
"<H1>Test server results</H1><BODY>"
"Size = %d<BR>"
"Version = %.8x<BR>"
"ConnID = %.8x<BR>"
"Method = %s<BR>"
"Query = %s<BR>"
"PathInfo = %s<BR>"
"PathTranslated = %s<BR>"
"TotalBytes = %d<BR>"
"AvailableBytes = %d<BR>"
"ContentType = %s<BR><BR>"
"<H1>Calls to GetServerVariable</H1>",
pECB->cbSize, pECB->dwVersion, pECB->ConnID,
pECB->lpszMethod, pECB->lpszQueryString,
pECB->lpszPathInfo, pECB->lpszPathTranslated,
pECB->cbTotalBytes, pECB->cbAvailable,
pECB->lpszContentType);
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize);
AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>");
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_HOST", &Buffer, &StrSize);
VarString += "REMOTE_HOST = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_USER", &Buffer, &StrSize);
VarString += "REMOTE_USER = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_NAME", &Buffer, &StrSize);
VarString += "SERVER_NAME = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_PORT", &Buffer, &StrSize);
VarString += "SERVER_PORT = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_PROTOCOL", &Buffer, &StrSize);
VarString += "SERVER_PROTOCOL = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_SOFTWARE", &Buffer, &StrSize);
VarString += "SERVER_SOFTWARE = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "HTTP_ACCEPT", &Buffer, &StrSize);
VarString += "HTTP_ACCEPT = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "URL", &Buffer, &StrSize);
VarString += "URL = " + AnsiString(Buffer) + "<BR><BR><BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "ALL_HTTP", &Buffer, &StrSize);
VarString += "ALL_HTTP = " + AnsiString(Buffer) + "<BR>";
strcat(HtmlInfo, VarString.c_str());
sprintf(ResultString,
"HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s </HTML>",
SIZE, HtmlInfo);
StrSize = strlen(ResultString);
pECB->WriteClient(pECB->ConnID, ResultString, &StrSize, 0);
return (HSE_STATUS_SUCCESS);
}
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
return 1;
}
To call this DLL, you should create an HTML script that contains the following
line:
<A HREF="/scripts/mystuff/isapivars.dll">Test One</A> <BR>
Of course, the actual path shown in your code may be somewhat different from what
I show here.
The HttpExtensionProc for this DLL is broken into two sections. The first
retrieves all the main fields from the EXTENSION_CONTROL_BLOCK, and the
second goes to town on one particular field, which is a function called GetServerVariable.
The code that parses the main fields of the EXTENSION_CONTROL_BLOCK is
fairly straightforward:
sprintf(HtmlInfo,
"<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>"
"<H1>Test server results</H1><BODY>"
"Size = %d<BR>"
"Version = %.8x<BR>"
"ConnID = %.8x<BR>"
"Method = %s<BR>"
"Query = %s<BR>"
"PathInfo = %s<BR>"
"PathTranslated = %s<BR>"
"TotalBytes = %d<BR>"
"AvailableBytes = %d<BR>"
"ContentType = %s<BR><BR>"
"<H1>Calls to GetServerVariable</H1>",
pECB->cbSize, pECB->dwVersion, pECB->ConnID,
pECB->lpszMethod, pECB->lpszQueryString,
pECB->lpszPathInfo, pECB->lpszPathTranslated,
pECB->cbTotalBytes, pECB->cbAvailable,
pECB->lpszContentType);
This code is nothing more than a simple call to sprintf. The goal is
simply to take the fields of the EXTENSION_CONTROL_BLOCK and mirror them
back to the user's browser. To do so, all you need to do is set up a legitimate HTML
document and then add a human-readable version of the struct to the body of the form.
The output from this part of the code looks like the screen shot shown in Figure
26.1.
FIGURE
26.1. The main fields of the EXTENSION_CONTROL_BLOCK
shown in a browser.
Working with GetServerVariable is a bit more complicated. As a result,
I will give this description a whole section so that it can have plenty of room in
which to knock about.
The GetServerVariable and ReadClient
Routines
You can use GetServerVariable to retrieve information from a server just
as you would request information inside a CGI application. Here is an example of
calling the routine:
#define SIZE 2048;
...
char Buffer[SIZE];
...
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize);
AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>");
This function takes a connection ID in the first parameter, a constant in the
second parameter, a buffer in the third parameter, and the length of the buffer in
the fourth parameter:
BOOL WINAPI GetServerVariable(
HCONN hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSizeofBuffer
);
As a rule, you will have to reset the fourth parameter before each call to this
function because the function itself returns the length of the string found in the
lpvBuffer parameter in the lpdwSizeOfBuffer parameter. You definitely
don't want to raise any exceptions or cause any errors to occur in your DLL, so I
suggest playing it safe when calling this function.
The preceding code first sets the length of the buffer that will hold the information
retrieved from the server. It then calls the server and asks for information. In
this case, it asks for the content length of the information sent by the server.
You can pass the following strings in the second parameter of GetServerVariable:
| AUTH_TYPE |
Type of authentication used. |
| CONTENT_LENGTH |
Number of bytes you can expect to receive from the client. |
| CONTENT_TYPE |
Type of information in the body of a POST request. |
| PATH_INFO |
Trailing part of the URL after the script name. |
| PATH_TRANSLATED |
PATH_INFO with any virtual pathname expanded. |
| QUERY_STRING |
Info following the "?" in the URL. |
| REMOTE_ADDR |
IP address of the client (could be a gateway or firewall). |
| REMOTE_HOST |
Hostname of the client or agent of the client. |
| REMOTE_USER |
Username supplied by the client and authenticated by the server. |
| UNMAPPED_REMOTE_USER |
Username before ISAPI mapped to an NT user account. |
| REQUEST_METHOD |
The HTTP request method. |
| SCRIPT_NAME |
The name of the script program being executed. |
| SERVER_NAME |
The server name. |
| SERVER_PORT |
The TCP/IP port on which the request was received. |
| SERVER_PORT_SECURE |
If the request is on the secure port, then this will be 1; otherwise, it
is 0. |
| SERVER_PROTOCOL |
Usually HTTP/1.0. |
| SERVER_SOFTWARE |
The name of the server software. |
| ALL_HTTP |
All headers not already parsed into one of the previous variables. |
| HTTP_ACCEPT |
The special-case HTTP header. |
| URL |
New for version 2.0. The base portion of the URL. |
You can find additional information on these variable names in the Microsoft online
help.
You can see examples of the type of information returned by calling GetServerVariable
in Figure 26.2. Note that this screen shot simply shows the lower half of the window
shown in Figure 26.1.
FIGURE
26.2. The results of making several repeated
calls to GetServerVariable.
Note that many of the preceding pieces of information are automatically passed
in the EXTENSION_CONTROL_BLOCK record. Therefore, you usually do not need
to call GetServerVariable, but you can if you need to, particularly if you
want to retrieve information with ReadClient and need to know how much information
to read.
Most of the time, you don't need to call ReadClient. However, if the
amount of data being sent by the browser is larger than 48KB, you will need to call
ReadClient to get the rest of the data.
The DLLEntryPoint
Before completing the discussion of how this DLL works, I want to take a moment
to get some housekeeping chores out of the way. In particular, I want to mention
the DLLEntryPoint routine, which appears at the bottom of the DLL.
All DLLs have an entry point that is called automatically. You don't have to respond
to this entry point, but doing so is usually a good idea. In this case, I simply
open a text file for debugging purposes:
#pragma argsused
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
switch (reason)
{
case DLL_PROCESS_ATTACH:
out = fopen("c:\\test.txt", "w+");
fprintf(out,"hello");
break;
case DLL_PROCESS_DETACH:
fprintf(out,"goodbye");
fclose(out);
break;
default:
break;
}
return (TRUE);
}
Nothing about the code shown here is mandatory. You don't have to open a text
file and write to it; the code serves no other purpose than to give you a simple
means of debugging your DLL. In particular, it creates a text file on the server
to leave a record of your DLL's behavior. This file is helpful, particularly if you're
learning ISAPI and have problems simply creating a valid DLL that exports the key
functions.
NOTE: You can debug an ISAPI DLL by using
several different means. One rather fancy method is to load the entire server into
the stand-alone debugger, load your ISAPI DLL, and then set a break point inside
it.
Although effective, the technique described in the preceding paragraph can be overkill.
My suggestion is not to be too proud about resorting to the old-fashioned technique
of creating a text file and writing to it. You can produce very detailed reports
in this fashion, and they can show you exactly what is going on in your DLL. The
only flaw in this system is that entering all those fprintf statements takes
a bit of time.
The example shown here has a more valuable purpose then merely showing one rather
simple-minded way to debug an ISAPI DLL. In particular, it reminds you of the proper
way to handle the entry point of a DLL. In addition to the DLL_PROCESS_ATTACH
and DLL_PROCESS_DETACH notifications are two others called DLL_THREAD_ATTACH
and DLL_THREAD_DETACH. Because multiple clients could be using your DLL
at the same time, DLL_THREAD_ATTACH can be a very important entry point
to use when you're debugging or constructing your DLL.
Getting Information from a Submit
Button
Often you get information sent to you from an HTML form that has a Submit button
on it. As long as this information is shorter than 49KB, you can assume that it will
be available in the lpbData field of TExtensionControlBlock. Otherwise,
you will need to call ReadClient. Here is how you would typically read the
information from the pointer passed in this field:
AnsiString S;
if (pECB->lpbData != NULL)
{
S = (char *)pECB->lpbData;
S = Parse(S);
}
else
S = "Error occurred on get from lpbData field";
This code first checks to see if lpbData is non-NULL. This type
of conservative coding is a necessity in ISAPI, as you don't want errors to be occurring
way over on the server side, where it is hard to see what is going on. The fragment
shown here then typecasts lpbData so that you can place its contents in
a variable of type AnsiString. It then passes the string to a user-defined
function called Parse that handles the string passed by the server. If something
goes wrong, the string variable is set equal to an error message and then returned
to the user so he or she can view it in a browser.
If you want to see exactly what information is available in the lpbData
field, you can use the following two functions to echo the data back to your Web
browser:
AnsiString SetupHeader(AnsiString &ResultString, AnsiString S, int &Len)
{
char *HeaderInfo = "HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s </HTML>";
ResultString.SetLength(S.Length() + strlen(HeaderInfo) + 1);
Len = ResultString.Length();
sprintf(ResultString.c_str(), HeaderInfo, Len, S);
return ResultString;
}
//HttpExtensionProc callback definition
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
AnsiString ResultString;
int ResultLen;
char *IsapiLogText = "Mirror lpbData";
strcpy(pECB->lpszLogData,IsapiLogText);
pECB->dwHttpStatusCode = 200;
AnsiString S;
if (pECB->lpbData != NULL)
{
S = (char *)pECB->lpbData;
}
else
S = "Error occurred get lpbData field";
SetupHeader(ResultString, S, ResultLen);
pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &(DWORD)ResultLen, 0);
return (HSE_STATUS_SUCCESS);
}
The first function, called SetupHeader, is just a utility routine that
automates the process of setting up a header. It forces you to pass in the variable
that is sent in the third parameter of WriteClient. I do this simply to
help remind myself that I have to initialize this variable before passing it to the
server.
The second routine simply mirrors the lpbData field back to the user
of the DLL. On the CD that accompanies this book, you will find a DLL called MirrorData
and an HTML form called MirrorDataTest.htm, which looks like this:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=iso-8859-1">
<meta name="GENERATOR" content="Microsoft FrontPage 2.0">
<title>MirrorDataTest</title>
</head>
<body bgcolor="#0000FF" text="#00FFFF">
<h1>View lpbData Test</h1>
<form action="/scripts/Books/BUnleash/MirrorData.dll"
method="POST">
<p>Enter some information: </p>
<p><textarea name="SendData" rows="16" cols="74"></textarea></p>
<p>Enter More Information</p>
<p><input type="text" size="76" name="SendData"></p>
<p><input type="submit" name="SendData" value="Submit"></p>
</form>
</body>
</html>
You can use this code to test the HttpExtensionProc that mirrors back
the value of lpbData. Note that the HTML form contains two fields for entering
text, the first called SendData and the second called MoreData.
If you have only one field and you type Fast into it, the DLL would mirror
back the following to the user:
SendData=Fast&SendData=Submit
If you have two fields, as in the HTML code shown here, then the following would
be mirrored back if the first field contains Fast and the second contains
Loose:
SendData=Fast&MoreData=Loose&SendData=Submit
If you break down this text into three separate fields, they would look like this:
SendData=Fast
&MoreData=Loose
&SendData=Submit
This text says, in effect, the SendData field has the word Fast
in it, the MoreData field has the word Loose in it, and the SendData
button is a Submit button.
Responding to a Submit Button
Assume that you have an HTML form with the code shown in Listing 26.3.
Listing 26.3. The HTML code for
a Web page that uses ISAPI to interact with the user.
<html>
<head>
<title>Talking ISAPI Test</title>
</head>
<body bgcolor="#0000FF" text="#00FFFF">
<h1>Talking ISAPI Test</h1>
<p>Press this button to see more of the simple ISAPI test:</p>
<form action="/scripts/Books/BUnleash/IsapiTalk.dll"
method="POST">
<p>Enter your name: <input type="text" size="20"
name="SendName"></p>
<p><input type="submit" name="SendName" value="Submit"></p>
</form>
</body>
</html>
This code will produce a form that contains a text area where the user can enter
his or her name and a button called Submit. Given this form, you can expect the lpbData
field to contain the following string, assuming the user enters the word Sammy
in the name field:
SendName=Sammy&SendName=Submit
To understand what is happening here, note the BODY of the HTML statement
composed on the server as reflected in the following excerpt from the SetUpResString
function shown previously:
`<BODY>lpbData = %s </BODY>' +
If you study the code in the HttpExtensionProc function, you will see
that it uses the Format routine to substitute the value of ECB.lpbData
for the %s variable in the preceding piece of code. (If you don't understand
how Format works, see the BCB documentation, or my references to this method
in Chapter 3, "C++Builder and the VCL.")
After you get the information from the form in the lpbData parameter,
you can parse it and return information to the user. For example, you could extract
the number 23 from the preceding example and then square it and return it to the
user. Doing so would in effect allow you to get information from the user, namely
a number, perform a mathematical action on the number, and then return the result
to the user. This means you're creating dynamic, interactive Web pages on-the-fly,
which is the current sine qua non of Internet programming!
Listing 26.4 shows the complete code for a program that will reply to a user who
enters his or her name into a page of a Web browser and submits it to the DLL. The
HTML that accompanies this program is shown in Listing 26.5.
Listing 26.4. The code for the ISAPITalk
DLL.
///////////////////////////////////////
// IsapiTalk.cpp
// Mirror back the information sent to an ISAPI DLL by the server
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <fstream.h>
#include <dir.h>
#pragma hdrstop
#include "..\..\utils\Httpext.h"
USERES("IsapiTalk.res");
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL");
return (TRUE);
};
// Remember: IOStream deals with OS paths, not relative path to server!
BOOL GetResultString(AnsiString &Result, AnsiString Path)
{
fstream InFile;
AnsiString FileName(Path + "\\BUnleash\\TalkTest\\TalkReply.htm");
char ch;
char S[500];
InFile.open(FileName.c_str(), ios::in, filebuf::openprot);
if (!InFile)
{
// If we couldn't get file, then what directory were we in?
Result = "<H>Error reading stream!</H>";
FileName = ExtractFilePath(FileName);
if (chdir(FileName.c_str()) == 0)
{
getcurdir(0, S);
Result += S;
}
else
Result += "Could not get file, nor find current directory!";
return FALSE;
}
while (InFile.get(ch))
Result += ch;
return TRUE;
}
AnsiString Parse(AnsiString &S)
{
int i = S.Pos("&");
S.SetLength(i - 1);
S = strrev(S.c_str());
i = S.Pos("=");
S.SetLength(i - 1);
S = strrev(S.c_str());
return S;
}
//HttpExtensionProc callback definition
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
AnsiString Header("HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length:%d\nContent:\n\n");
AnsiString ResultString;
int resultLen;
char *IsapiLogText = "ISAPI Talk";
strcpy(pECB->lpszLogData,IsapiLogText);
pECB->dwHttpStatusCode = 200;
// build the HTML result string
AnsiString S;
if (pECB->lpbData != NULL)
{
S = (char *)pECB->lpbData;
S = Parse(S);
}
else
S = "Error occurred get lpbData field";
AnsiString Date(DateToStr(Now()));
AnsiString Time(TimeToStr(Now()));
GetResultString(ResultString, pECB->lpszPathTranslated);
ResultString = Format(ResultString, OPENARRAY(TVarRec, (S, Date, Time)));
resultLen = ResultString.Length();
Header = Format(Header, OPENARRAY(TVarRec, (resultLen)));
pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &(DWORD)resultLen, 0);
return (HSE_STATUS_SUCCESS);
}
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
return 1;
}
Listing 26.5. The HTML file,
called TalkReply.htm, used in the reply generated by the ISAPITalk DLL.
<html>
<head>
<title>This is a simple htm file</title>
</head>
<body bgcolor="#0000FF" text="#00FFFF">
<h1>Talking HTML Reply</h1>
<p> </p>
<p>Welcome, %s, to the talking HTML file!</p>
<p>It's remarkable that you should drop by today, %s, at %s (PST), as
I was just thinking about you. </p>
</body>
</html>
This program allows an HTML browser to work interactively with a Web server. Screen
shots of before and after a query is made are shown in Figure 26.3 and Figure 26.4,
respectively.
When you're working with this program, note that I assume you have placed the
TalkReply.htm program in the following location relative to the root of
your Web:
\\BUnleash\\TalkTest\\TalkReply.htm
The ISAPITalk DLL might receive the following string from the user who
clicks the Submit button, asking that a number be squared:
SendName=Sammy&SendName=Submit
FIGURE
26.3. Preparing to query the ISAPITalk
DLL.
FIGURE
26.4. The results of querying the ISAPITalk
DLL.
Given this input, the preceding code would return the following string to the
user across the Internet:
Welcome, Sammy, to the talking HTML file!
It's remarkable that you should drop by today, 2/15/97,
at 10:39:29 PM (PST), as I was just thinking about you.
In short, the user enters the string "Sammy", and the ISAPI
DLL mirrors this information back and even adds in the current date and time as it
is reported on the server side. This process sounds trivial, but the key issue here
is that this activity is taking place dynamically on the Internet.
The function that parses the data sent by the user looks like this:
AnsiString Parse(AnsiString &S)
{
int i = S.Pos("&");
S.SetLength(i - 1);
S = strrev(S.c_str());
i = S.Pos("=");
S.SetLength(i - 1);
S = strrev(S.c_str());
return S.
}
The code uses the strrev function from the Standard C Library to expedite
the task of parsing out all the unnecessary data sent by the server.
The ISAPITalk program does not imbed any HTML code inside the C++ code that makes
the DLL run. Instead, it reads in an HTML file and uses the VCL Format function
to fill in the blank fields of the form shown in Listing 26.5.
The code for reading in the HTML file goes to considerable lengths to properly
handle any errors that occur:
if (!InFile)
{
// If we couldn't get file, then what directory were we in?
Result = "<H>Error reading stream!</H>";
FileName = ExtractFilePath(FileName);
if (chdir(FileName.c_str()) == 0)
{
getcurdir(0, S);
Result += S;
}
else
Result += "Could not get file, nor find current directory!";
return FALSE;
}
The issue here is that the file you want to load might be missing or might be
in another directory than the one you suppose. The code shown here attempts to find
the directory in which the DLL expects the HTML file to reside and to mirror that
information back to the user on the off chance that an error occurs. This information
can help you fix any broken links in your program without too much fussing around.
NOTE: The reason the ISAPITalk program
reads in the HTML for the reply form from a file is simply that I don't like embedding
HTML code inside C++ code. No technical reason prevents me from embedding HTML inside
C++, but doing so does tend to be confusing. In particular, embedding the code makes
it difficult for you to generate an attractive form that has just the proper look
you want to produce. If you separate the HTML from your C++ code, you can use your
favorite HTML editor to produce just the look you want.
The discussion of the ISAPITalk program has one final leg that needs to be completed
before the race is done. This last portion of the journey involves getting the proper
path to the HTML file that you read in while generating the reply form.
The Server Path Versus the OS Path
When you're writing ISAPI applications, you need to distinguish between the relative
path you use when talking to the Web server and the absolute OS path you use when
executing functions inside your DLL. Server paths should always be relative to the
root of your Web, but OS paths are concerned with the current drive and directory.
If you embed the name of an HTML file in a hyperlink that is part of a Web path,
you're dealing with relative server paths. If you call the C library chdir
or getcurdir functions, then you're working with an OS path. For example,
I need to pass an OS-based path to the function in the ISAPITalk program that reads
an HTML file from disk.
Finding out the current OS path is relatively easy because ISAPI passes the absolute
path to the root of the Web in the lpszPathTranslated field of the EXTENSION_CONTROL_BLOCK.
For example, if the root of your Web is C:\WEBSHARE\WWWROOT, that path is
passed to you in the lpszPathTranslated field.
The relative path you use in your Web pages is something that you usually determine
while laying out the Web itself. Sometimes you have to refer to this path in your
code. In particular, you will often reference this path if you're creating HTML on-the-fly.
Most of the time, you simply have to determine the correct path by looking at the
position of your files relative to the root of your Web.
Here is an example of a relative path used in an HTML file:
\\BUnleash\\TalkTest\\TalkReply.htm
Here is the OS path to the same file:
C:\WEBSHARE\WWWROOT\\BUnleash\\TalkTest\\TalkReply.htm
Whatever you do, don't ever embed full OS pathnames in either your HTML or your
ISAPI DLLs. If you do, then you will have to edit your code and your HTML whenever
you change the location of your Web site. That is much too much work. Furthermore,
from a security point of view, telling the users of your Web any more than they have
to know about the actual layout of your hard drive is probably not a good idea.
To find out more about this subject, you can examine the ISAPITalk DLL
on the CD that accompanies this book. This program provides an example of finding
out the exact path to a file on the server so that you can load it into your DLL.
That's most of what I want to say about ISAPI in this chapter. This information
should be enough to get you up and running and having some fun with this great technology.
Retrieving Data with an ISAPI DLL
In this section you will see a simple example for retrieving data from an ISAPI
DLL. The CGI examples featured later in this book go into more depth on database
techniques to use in this type of program. You can easily convert the CGI code into
code that can be used with ISAPI.
The sample DLL shown in Listing 26.6 retrieves all the data from the Country table
in the BCDEMOS database and shows it to the user in an HTML table.
Listing 26.6. The IsapiData program
shows how to retrieve data from a database and display it in a browser.
///////////////////////////////////////
// IsapiData
// Return Database rows from an ISAPI DLL
// Copyright (c) 1997 by Charlie Calvert
// Thanks to David Intersimone and Roland Fernandez
//
#include <vcl\vcl.h>
#include "..\..\utils\Httpext.h"
#pragma hdrstop
AnsiString TableToHtml(TDataSet *Table)
{
int i;
AnsiString Result;
Result = "<TABLE BORDER>\n";
for (i = 0; i < Table->FieldCount; i++)
{
Result = Result + Format("<TH>%s</TH>",
OPENARRAY(TVarRec, (Table->Fields[i]->FieldName.c_str())));
}
while (!Table->Eof)
{
Result = Result + "<TR>";
for (i = 0; i < Table->FieldCount; i++)
{
Result = Result + Format("<TD>%s</TD>",
OPENARRAY(TVarRec, (Table->Fields[i]->AsString.c_str())));
}
Result = Result + "</TR>\n";
Table->Next();
}
return Result + "</TABLE>\n";
}
AnsiString _stdcall _export GetData()
{
TTable *Table;
AnsiString Result;
Table = new TTable(Application);
Table->DatabaseName = "DBDEMOS";
Table->TableName = "Country";
Table->Open();
Result = TableToHtml(Table);
Table->Close();
delete Table;
return Result;
}
BOOL WINAPI _export GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL");
return (TRUE);
};
void TextWrite(AnsiString S)
{
FILE *F;
S = S + "\n";
F = fopen("c:\\foo.txt", "w+");
fprintf(F, S.c_str());
fclose(F);
}
void TextAppend(AnsiString S)
{
FILE *F;
S = S + "\n";
F = fopen("c:\\foo.txt", "a+");
fprintf(F, S.c_str());
fclose(F);
}
DWORD WINAPI _export HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
AnsiString Header("HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length: "
" %d\nContent:\n\n");
AnsiString ResultString;
int resultLen;
char *IsapiLogText = "ISAPI1 - Simple BC++ ISAPI Extension DLL";
strcpy(pECB->lpszLogData,IsapiLogText);
pECB->dwHttpStatusCode = 200;
ResultString = GetData();
Header = Format(Header, OPENARRAY(TVarRec, (ResultString.Length())));
Header = Header + ResultString;
resultLen = Header.Length();
pECB->WriteClient(pECB->ConnID, Header.c_str(), &(DWORD)resultLen, 0);
return (HSE_STATUS_SUCCESS);
}
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
switch (reason)
{
case DLL_PROCESS_ATTACH:
// TextWrite("DllLoaded");
break;
case DLL_PROCESS_DETACH:
// TextAppend("Dll UnLoaded");
break;
}
return 1;
}
BCB makes creating database-centered DLLs very simple. To open a table, all you
have to do is write the standard code for creating and opening a database:
AnsiString _stdcall _export GetData()
{
TTable *Table;
AnsiString Result;
Table = new TTable(Application);
Table->DatabaseName = "DBDEMOS";
Table->TableName = "Country";
Table->Open();
Result = TableToHtml(Table);
Table->Close();
delete Table;
return Result;
}
In this example I create and open a table, being sure to set up the DatabaseName
and TableName properties. I then call a special routine specifically designed
to convert the data in a table into an HTML form. Finally I close and delete the
table.
If you wanted to create a simpler test program, you could simply access the first
field of data in the table:
Result = Table->Fields[0]->AsString.c_str()
As you will see in the CGI examples presented next, there is no reason why you
cannot add a DataModule to this kind of program. In fact, you can extend
ISAPI DLLs to the point where they handle relatively complex database chores such
as processing queries or editing records. I want to thank David Intersimone and Roland
Fernandez for showing me that these applications could be considerably simpler than
I thought at first.
Introduction to CGI Programming
CGI programming is similar to ISAPI programming, except that you create executables
rather than DLLs, and it works from inside any respectable server, rather than only
from Microsoft's servers. To get input from a browser, you read the DOS environment
variables.
Here is an example of reading a CGI variable:
char * S;
S = getenv("QUERY_STRING");
Here is an example of writing to a browser from a CGI application:
printf("This string will appear in a browser.");
As you can see, writing this code is not exactly rocket science. Because of its
simplicity, I will whip right through several simple examples and then get on to
some database applications. I don't mean to denigrate CGI because it is so simple
to use. I don't like anything more than simple APIs, and CGI is one of the simplest.
This technology is very powerful, and like ISAPI, it can quickly upgrade a Web site
from something pretty average to something pretty spectacular.
A Simple CGI Example
CGI is so simple that I almost don't need to include the most basic of possible
examples. However, enough things can go wrong with Web-based applications that you
need one simple example that you know will work. The code in Listing 26.7 provides
that example.
Listing 26.7. The SimpleCGI application.
///////////////////////////////////////
// SimpleHelp.cpp
// Using an object to make CGI easier
// Copyright (3) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
char S[] =
"TO HIS COY MISTRESS<P>\n"
"Had we but world enough and time,<BR>\n"
"This coyness, lady, were no crime.<BR>\n"
"We would sit down, and think which way<BR>\n"
"To walk, and pass our long love's day.<BR>\n"
"Thou by the Indian Ganges' side<BR>\n"
"Shouldst rubies find: I by the tide<BR>\n"
"Of Humber would complain. I would<BR>\n"
"Love you ten years before the Flood:<BR>\n"
"And you should if you please refuse<BR>\n"
"Till the conversion of the Jews.<BR>\n"
"My vegetable love should grow<BR>\n"
"Vaster than empires, and more slow.<BR>\n"
"An hundred years should go to praise<BR>\n"
"Thine eyes, and on thy forehead gaze.<BR>\n"
"Two hundred to adore each breast:<BR>\n"
"But thirty thousand to the rest.<BR>\n"
"An age at least to every part,<BR>\n"
"And the last age should show your heart.<BR>\n"
"For lady, you deserve this state;<BR>\n"
"Nor would I love at lower rate.<BR>\n"
"But at my back I always hear<BR>\n"
"Time's winged chariot hurrying near:<BR>\n"
"And yonder all before us lie<BR>\n"
"Deserts of vast eternity.<BR>\n"
"Thy beauty shall no more be found,<BR>\n"
"Nor, in thy marble vault, shall sound<BR>\n"
"My echoing song; then worms shall try<BR>\n"
"That long preserved virginity:<BR>\n"
"And your quaint honour turn to dust;<BR>\n"
"And into ashes all my lust.<BR>\n"
"The grave's a fine and private place,<BR>\n"
"But none, I think , do there embrace.<BR>\n"
"Now therefore, while the youthful hue<BR>\n"
"Sits on thy skin like morning dew,<BR>\n"
"And while thy willing soul transpires<BR>\n"
"At every pore with instant fires,<BR>\n"
"Now let us sport us while we may;<BR>\n"
"And now, like am'rous birds of prey,<BR>\n"
"Rather at once our time devour,<BR>\n"
"Than languish in his slow chapped power<BR>\n"
"Let us roll all our strength, and all<BR>\n"
"Our sweetness, up into one ball:<BR>\n"
"And tear our pleasures with rough strife,<BR>\n"
"Through the iron gates of life.<BR>\n"
"Thus, though we cannot make our sun<BR>\n"
"Stand still, yet we will make him run.<BR>\n"
"-- Andrew Marvell";
void main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("\nContent-type: text/html\n\n");
printf("<html>\n");
printf("<body>\n");
printf("<body bgcolor=\"\#0000FF\" text=\"\#00FFFF\" link=\"\#FFFF00\""
"vlink=\"\#00FF00\" alink=\"\#FFFFFF\">");
printf(S);
printf("</body>\n");
printf("</html>\n");
}
This console application has no main form; its output appears only in a browser,
as shown in Figure 26.5.
Figure 26.5
The Simple CGI application.
To create the SimpleCGI application, start a new project and remove the
main form. Save the project file in its own directory, and then choose Options |
Project | Linker and set the Application Type to Console Application. You can also
use File | New and select Console App from the App Expert dialog.
Remove all the WinMain business, and replace it with a simple DOS-style
main block:
void main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("\nContent-type: text/html\n\n");
printf("<html>\n");
printf("<body>\n");
printf("...the sessions of sweet silent thought...");
printf("</body>\n");
printf("</html>\n");
}
This complete CGI application needs only be compiled and then copied to a server.
You call a CGI application exactly as you would an ISAPI DLL:
<p><a href="/scripts/books/bunleash/SimpleCgi.exe">SimpleCgi.exe</a>
Two complete HTML files for use with this chapter are available in the root of
the Chap26 directory on the CD that accompanies this book. You might, of
course, have to edit the paths shown in these files.
Retrieving the Variables Passed
to a CGI Application
The variables passed to a CGI application can be retrieved from the DOS environment.
You see these same types of variables when you type Set at the DOS prompt.
You can use the getenv or _environ function to retrieve this data.
NOTE: Ghastly function names like getenv
and the deplorable _environ are the result of a bygone time when compilers
had a limited ability to work with long identifiers. Furthermore, there was a need
to try to save every byte in the applications being produced.
Despite the ugly function names, those were romantic and exciting times. It was a
period when all PC-based computer technology was new, and the industry was experiencing
dynamic growth.
Despite the romance of that era, our much more complex contemporary programming environments
require more careful habits. In modern programming, these functions would have easier-to-understand
names like GetEnvironment and Environment.
I sometimes catch myself wondering how many wasted hours have been lost, on a global
scale, as a result of the horrible naming conventions and ludicrous capitalization
standards that used to exist. On the other hand, I also can't help but look back
fondly, on the smaller, more manageable APIs that existed in those times, and on
the highly charged, and much more friendly, atmosphere that prevailed.
The CGIVars application found on the CD that accompanies this book shows how to
iterate through all the environment variables passed to a CGI application. The output
from that program is shown in Figure 26.6, and the source for the program is shown
in Listing 26.8 through Listing 26.10.
FIGURE
26.6. The output from the CGIVars application.
Listing 26.8. The CGIVars application
shows how to retrieve all the variables passed to a CGI application.
///////////////////////////////////////
// CGIVars.cpp
// Using an object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "CGIHelp1.h"
USEUNIT("..\..\Utils\CGIHelp1.cpp");
void main(void)
{
TCGIHelp CGI;
CGI.Header("World Enough and Time");
CGI.ShowEnv();
}
Listing 26.9. The TCGIHelp object
provides support for CGI application. This is the header for the module.
///////////////////////////////////////
// CGIHelp1.h
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef CGIHelp1H
#define CGIHelp1H
enum TColorType {ctLightBlue, ctRedYellow, ctBlueYellow, ctGreenBlack};
class TCGIHelp
{
public:
TCGIHelp();
~TCGIHelp();
void HTMLHeader();
void Header(AnsiString S);
AnsiString GetQuery();
void Colors(TColorType ColorType);
void ShowEnv();
};
#endif
Listing 26.10. The main source
file for the TCGIHelp utility.
///////////////////////////////////////
// CGIHelp1.cpp
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "CGIHelp1.h"
TCGIHelp::TCGIHelp()
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("\nContent-type: text/html\n\n");
printf("<html>\n");
printf("<body>\n");
}
TCGIHelp::~TCGIHelp()
{
printf("</body>\n");
printf("</html>\n");
}
void TCGIHelp::Header(AnsiString S)
{
AnsiString Temp = "<H1>" + S + AnsiString("</H1>");
printf(Temp.c_str());
}
AnsiString TCGIHelp::GetQuery()
{
char *Query;
Query = getenv("QUERY_STRING");
if (Query)
return Query;
else
return "";
}
void TCGIHelp::Colors(TColorType ColorType)
{
switch(ColorType)
{
case ctLightBlue:
printf("<body bgcolor=\"\#0000FF\""
"text=\"\#00FFFF\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
break;
case ctRedYellow:
printf("<body bgcolor=\"\#FF0000\""
"text=\"\#FFFF\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
break;
case ctBlueYellow:
printf("<body bgcolor=\"\#0000FF\""
"text=\"\#FFFF\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
break;
case ctGreenBlack:
printf("<body bgcolor=\"\#00FF00\""
"text=\"\#000000\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
break;
}
}
void TCGIHelp::ShowEnv()
{
int i =0;
while (_environ[i])
printf("%s<BR>", _environ[i++]);
}
The main body of this console application is simple:
void main(void)
{
TCGIHelp CGI;
CGI.Header("Current Environment");
CGI.ShowEnv();
}
The code is brief because I'm using a small helper object that gets many of the
worst chores out of the way for me:
class TCGIHelp
{
public:
TCGIHelp();
~TCGIHelp();
void HTMLHeader();
void Header(AnsiString S);
AnsiString GetQuery();
void Colors(TColorType ColorType);
void ShowEnv();
};
This object performs the basic chores needed to run a CGI application. It is also
the kind of simple object that can be improved and enlarged as you see fit. In other
words, you should feel free to add methods to this object as you find a need for
them.
The constructor for TCGIHelp takes care of sending off a MIME header
that browsers examine so they can know what to do with the HTTP code that is being
streamed toward them:
TCGIHelp::TCGIHelp()
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("\nContent-type: text/html\n\n");
printf("<html>\n");
printf("<body>\n");
}
The equally simple destructor outputs the lines that mark the end of an HTML form:
TCGIHelp::~TCGIHelp()
{
printf("</body>\n");
printf("</html>\n");
}
Other functions perform mundane tasks such as automatically formatting a header:
void TCGIHelp::Header(AnsiString S)
{
AnsiString Temp = "<H1>" + S + AnsiString("</H1>");
printf(Temp.c_str());
}
If you look at the source, you will see a method called colors that sets
the background and foreground colors for a form. A simple enumerated type can be
passed to the function so that you can set up the colors you need:
enum TColorType {ctLightBlue, ctRedYellow, ctBlueYellow, ctGreenBlack};
You can create more colors if you like. Each color statement sets the background
color, the text color, and the link colors for the times before, during, and after
they have been selected:
printf("<body bgcolor=\"\#0000FF\""
"text=\"\#00FFFF\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
The numbers representing the colors work just like the standard Windows RGB colors:
#0000FF: Blue
#00FF00: Green
#FF0000: Red
You can find more details in Chapter 7, "Graphics."
The method that actually iterates through the environment is extremely simple:
void TCGIHelp::ShowEnv()
{
int i =0;
while (_environ[i])
printf("%s<BR>", _environ[i++]);
}
Not much I can say about that one. It's just simple C 101 code from your first
programming course.
If you want to retrieve an individual item from the environment, you can use this
method:
AnsiString TCGIHelp::GetQuery()
{
char *Query;
Query = getenv("QUERY_STRING");
if (Query)
return Query;
else
return "";
}
The main purpose of this code is to return a friendly AnsiString rather
than force the user to gamble on using a NULL-terminated string. This place
would, of course, be a particularly bad one to try to shave a few clock cycles off
an application by using NULL-terminated strings rather than AnsiStrings.
The reasons for this are twofold:
- 1. If you do make a mistake using a NULL-terminated string, you
will raise an exception, which will stop your application dead in the water because
it is running on another machine. Often the only way to recover is to reboot the
server. Later in the chapter, I will discuss how to treat exceptions raised in CGI
applications.
2. CGI applications are slow by their very nature because they run over the network.
As a result, nobody is going to notice if you shave a few nanoseconds here and a
few nanoseconds there. As a result, you should use safe techniques for handling strings
and think about other techniques for improving performance.
Given these considerations, I chose to return AnsiStrings from this method.
If no environment variable called QUERY_STRING exists, then the GetQuery
method returns an empty AnsiString; otherwise, it returns the string retrieved
from the system. getenv returns NULL if it cannot find the environment
string you request. The server is the one that sets up the environment that will
prevail inside the session where your application is launched.
NOTE: This section is not the place for
me to get into the whole subject of creating CGI applications that are as small and
as efficient as possible. You should be aware, however, that considerable thought
has gone into sensible ways of creating applications that will stay in memory so
that they do not have to be launched each time they are called. You can find books
on this subject, or you can turn to third parties such as HREF: www.href.com.
Creating Simple CGI Database Applications
You can easily create a BCB-based CGI application that handles databases. All
you have to do is allocate memory for a table, open it, and wrap its contents inside
HTML statements. An example is shown Figure 26.7.
FIGURE
26.7. The output for the CGIData application.
On the CD that accompanies this book, I include two applications that show the
basics of working with the TTable object inside a CGI application. I also
include one slightly more complex example that shows how to work with TQuery,
TDatamodule, and InterBase from inside a CGI application.
NOTE: For the code shown in this section,
I assume that you have the BDE set up on your server and that you or the installation
program has created a BCDEMOS alias for the BDE.
Later, I will show an application running against InterBase tables. In that case,
you will need to have both InterBase and the BDE running on your server. You could
run the local InterBase server in this case because you can, if you want, query InterBase
from the same machine on which the server resides. In other words, you can run both
the local InterBase and your Web server on the same machine.
If you're using InterBase, Oracle, or other SQL servers, you will find a limit to
the number of simultaneous connections you can support, depending on the nature of
the license for your database server. You can still have multiple connections to
your Web server, but you might be able to service only a limited number of requests
for data at any one time.
The source for the first of the CGI-based database applications is shown in Listing
26.11. This program depends on the TCGIHelp object shown in Listing 26.9.
Listing 26.11. The source for the
simplest possible case example of using a database code in a CGI application.
///////////////////////////////////////
// IsapiVars.cpp
// Mirror back the information sent to an ISAPI DLL by the server
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "..\..\utils\Httpext.h"
USERES("IsapiVars.res");
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL");
return (TRUE);
};
#define SIZE 2048
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
char ResultString[SIZE * 2];
char HtmlInfo[SIZE];
char Buffer[SIZE];
DWORD StrSize;
DWORD resultLen;
char *IsapiLogText = "ISAPIVars from C++ Builder";
strcpy(pECB->lpszLogData, IsapiLogText);
pECB->dwHttpStatusCode = 200;
sprintf(HtmlInfo,
"<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>"
"<H1>Test server results</H1><BODY>"
"Size = %d<BR>"
"Version = %.8x<BR>"
"ConnID = %.8x<BR>"
"Method = %s<BR>"
"Query = %s<BR>"
"PathInfo = %s<BR>"
"PathTranslated = %s<BR>"
"TotalBytes = %d<BR>"
"AvailableBytes = %d<BR>"
"ContentType = %s<BR><BR>"
"<H1>Calls to GetServerVariable</H1>",
pECB->cbSize, pECB->dwVersion, pECB->ConnID,
pECB->lpszMethod, pECB->lpszQueryString,
pECB->lpszPathInfo, pECB->lpszPathTranslated,
pECB->cbTotalBytes, pECB->cbAvailable,
pECB->lpszContentType);
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize);
AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>");
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_HOST", &Buffer, &StrSize);
VarString += "REMOTE_HOST = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_USER", &Buffer, &StrSize);
VarString += "REMOTE_USER = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_NAME", &Buffer, &StrSize);
VarString += "SERVER_NAME = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_PORT", &Buffer, &StrSize);
VarString += "SERVER_PORT = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_PROTOCOL", &Buffer, &StrSize);
VarString += "SERVER_PROTOCOL = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_SOFTWARE", &Buffer, &StrSize);
VarString += "SERVER_SOFTWARE = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "HTTP_ACCEPT", &Buffer, &StrSize);
VarString += "HTTP_ACCEPT = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "URL", &Buffer, &StrSize);
VarString += "URL = " + AnsiString(Buffer) + "<BR><BR><BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "ALL_HTTP", &Buffer, &StrSize);
VarString += "ALL_HTTP = " + AnsiString(Buffer) + "<BR>";
strcat(HtmlInfo, VarString.c_str());
sprintf(ResultString,
"HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s </HTML>",
SIZE, HtmlInfo);
StrSize = strlen(ResultString);
pECB->WriteClient(pECB->ConnID, ResultString, &StrSize, 0);
return (HSE_STATUS_SUCCESS);
}
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
return 1;
}
Because you're working in an application, as opposed to a DLL, the TApplication
object is available to you. This object helps set up the TSessions object
that is required by database applications. As a result, the conservative and safe
thing to do is to make sure that the Application->Initialize method is
called so that the database tools are set up properly:
Application->Initialize();
NOTE: This step makes ISAPI database applications
a bit tricky. ISAPI runs in a DLL, and you have to be careful setting up database
code in a DLL. In particular, you have to be sure the Sessions object is
properly initialized and that each table is assigned a Session.
After setting up the Application and Sessions objects, you are
free to open up a table. This application has no data module and no form, so you
must do all the work yourself:
Table = new TTable(Application);
Table->DatabaseName = "BCDEMOS";
Table->TableName = "Customer";
Table->Open();
This code allocates memory for the table, specifies the database and table the
application wants to use, and finally opens the table.
After the table is open, you can start retrieving data immediately. However, you
might want to put off this step just long enough to set up an HTML table:
printf("<TABLE BORDER>\n");
printf("<TH>%s</TH><TH>%s</TH>",
Table->Fields[0]->FieldName.c_str(),
Table->Fields[1]->FieldName.c_str());
This code calls the FieldName property of the TField object
to retrieve the name of each column in the database. This information is displayed
in the top row of the HTML table. As you can see, this example works with only the
first two columns. The CGIData2 program, shown in Listing 26.12, provides a generic
solution for converting any table to HTML.
The following code iterates through the database, displaying all the data from
the first two fields to the user:
while (!Table->Eof)
{
printf("<TR><TD>%s</TD><TD>%s</TD></TR>\n",
Table->Fields[0]->AsString.c_str(),
Table->Fields[1]->AsString.c_str());
Table->Next();
}
After the data is shown, I close the HTML table, close the TTable object,
and then delete the TTable object:
printf("</TABLE>\n");
Table->Close();
delete Table;
CGI database programming is obviously simple. You can, however, make the process
even easier. I will demonstrate these techniques in the next section.
Converting a Table to HTML
The CGIData2 application shows a technique for converting any arbitrary table
to HTML. The program also shows how to pass information to a CGI application so that
the user can choose the table he or she wants to display. The combination of these
two traits gives the user the ability to browse any table on your system over the
Internet.
The output from the CGIData2 program is shown Figure 26.8, and the source for
the program is shown in Listings 26.12 through 26.14. Note the URL passed to the
application in Figure 26.8. The information after the question mark specifies which
table the user wants to browse.
FIGURE
26.8. The output from the CGIData2 application
while looking at the Parts table from BCDEMOS.
Listing 26.12. The source for
the main module in the CGIData2 application.
///////////////////////////////////////
// CGIData2.cpp
// Databases and CGI
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "cgiDBhelp1.h"
USEUNIT("..\..\Utils\CGIHelp1.cpp");
USEUNIT("..\..\Utils\CGIDBHelp1.cpp");
void main(void)
{
TCGIDBHelp CGI;
CGI.Colors(ctGreenBlack);
char *Query;
TTable *Table;
Query = getenv("QUERY_STRING");
if (Query)
{
Application->Initialize();
Table = new TTable(Application);
Table->DatabaseName = "BCDEMOS";
Table->TableName = Query;
try
{
Table->Open();
CGI.Header(Table->TableName);
CGI.PrintTable(Table);
Table->Close();
delete Table;
}
catch(Exception &E)
{
printf("Could not open table: %s", E.Message.c_str());
}
catch(...)
{
printf("Could not open table");
}
}
else
{
printf("<H1>No query string available</H1>\n");
}
Application->Run();
}
Listing 26.13. The header for
the TCGIDBHelp file.
///////////////////////////////////////
// CGIDBHelp1.h
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef CGIDBHelp1H
#define CGIDBHelp1H
#include "CGIHelp1.h"
class TCGIDBHelp: public TCGIHelp
{
public:
void PrintTable(TDataSet *Table);
};
#endif
Listing 26.14. The TCGIDBHelp
object provides support for CGI database applications.
///////////////////////////////////////
// CGIDBHelp1.cpp
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "CGIDBhelp1.h"
void TCGIDBHelp::PrintTable(TDataSet *Table)
{
int i;
printf("<TABLE BORDER>\n");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str());
}
while (!Table->Eof)
{
printf("<TR>");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str());
}
printf("</TR>\n");
Table->Next();
}
printf("</TABLE>\n");
}
This application is designed to allow the user to interact with it dynamically.
In particular, this application retrieves the name of the table the user wants to
see from the environment variable called QUERY_STRING:
Query = getenv("QUERY_STRING");
if (Query)
{
... // Code omitted here
Table->TableName = Query;
}
else
{
printf("<H1>No query string available</H1>\n");
}
These strings are passed to the application if you append a question mark to the
URL that calls the CGI script and then append data after the question mark. For example,
the following HTML code would call this script without specifying any table name:
<a href="/scripts/books/bunleash/CgiData2.exe">CGIData2.exe</a>
You might have to change the path specified in this code, but it shows the general
formula for the code you should write. If you click the reference listed here, the
program would report an error, as shown in Figure 26.9.
FIGURE
26.9. The CGIData2 program returns an
error like this if the user does not specify the name of a table he or she wants
to view.
If you want to ask about the Customer database, you can edit the string so that
it looks like this:
/scripts/books/bunleash/CgiData2.exe?Customer">CGIData2.exe
This request retrieves the Customer table from the database. You do not,
of course, have to hard-code the name of each table you want to query into your form.
In fact, in this particular example, you don't even have to use an HTML form at all.
If you want, you can merely edit the raw HTML string in the Address field
of your browser, appending different files after the question mark to retrieve different
sets of data. For example, you could ask for the Customer, Orders,
Items, Events, Parts, or BioLife tables.
If CGIData2 finds a valid query string in the global environment, it tries to
open a table. If it can't find a string, then it prints a message to the user explaining
the problem:
printf("<H1>No query string available</H1>\n");
Figure 26.10 shows the message that the CGIData2 program returns to the browser
when the user enters an invalid table name.
FIGURE
26.10. The output from the CGIData2 program
when the user asks for a nonexistent table.
As you can see, the big danger here is that the user will pass in an invalid string.
If this happens, an exception will be raised, which can hang your server, at least
until you use a Task Manager to shut down the service.
NOTE: One way to ease the chore of debugging
CGI applications is to run against a server on your own system. For example, if you
are on Windows 95 and you use the Personal Web Server, then exceptions raised by
your application appear in message boxes that you can read directly on your screen.
You can then just click the OK button on the message so that the application will
terminate. This option is not available if you're running against an NT server that
resides on a separate machine. In fact, exceptions never appear on the screen on
most servers, and your application will hang, with no way for you to click the OK
button.
I'm sure most readers can guess that try..catch blocks are the way to
handle exceptions that occur on a server. In particular, the following code catches
exceptions that occur when you try to open a table or during the process of iterating
through the records of a table:
try
{
Table->Open();
CGI.Header(Table->TableName);
CGI.PrintTable(Table);
Table->Close();
delete Table;
}
catch(Exception &E)
{
printf("Could not open table: %s", E.Message.c_str());
}
catch(...)
{
printf("Could not open table");
}
}
In this case, I am assuming that all errors will be caused because the user passed
in an invalid string. However, the code will trap any VCL error and return a string
specifying what went wrong. On the off chance that the exception is not raised by
the VCL, I have a generic catch block that simply states that something
went wrong and that the table could not be opened. For more information, see Chapter
5, "Exceptions."
Notice that I also call Application->Run(). Calling this method appears
to be necessary if you want to handle exceptions properly in a database application.
The last chunk of code to look at from this application is the bit that converts
a table to HTML:
void TCGIDBHelp::PrintTable(TDataSet *Table)
{
int i;
printf("<TABLE BORDER>\n");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str());
}
while (!Table->Eof)
{
printf("<TR>");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str());
}
printf("</TR>\n");
Table->Next();
}
printf("</TABLE>\n");
}
This code is part of an object designed to work with database CGI applications.
I separate it from the TCGIHelp object because database code adds so much
to the size of a file. My theory is that you should include this file in your project
only if you're explicitly using database objects; otherwise, stick with the smaller
TCGIHelp file. TCGIDBHelp is a direct descendant of TCGIHelp,
so it inherits all of methods from TCGIHelp.
The code shown here starts by iterating through all the fields in the database
and places their names in the header for the table:
printf("<TABLE BORDER>\n");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str());
}
With this task out of the way, the code then iterates through the entire table,
filling out each row in its entirety:
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str());
}
That's all I'm going to say about the CGIData2 program. Once again, you can see
that publishing data over the Web is an easy operation. If you own BCB and run on
an NT Server, you can publish data all day long with very little effort. If you have
a smaller clientele, you can do the same thing with Windows 95 and the Personal Web
Server, or else you can turn to more powerful tools such as the WebSite Server provided
by O'Reilly.
O'Reilly is located at www.ora.com. Try http://www.ora.com/catalog/webpro/
for the professional version of the product and http://www.ora.com/catalog/web1.1/
for the inexpensive standard edition.
Using Queries, Data Modules, and
InterBase over the Web
The real power of CGI applications is made clear when you start running SQL queries
against data. For example, I took the Music.gdb database from Chapter 16,
"Advanced InterBase Concepts," and let the user run some queries against
it, as shown in Figure 26.11 and Figure 26.12. In particular, this application calls
some stored procedures designed to quickly pump out data that answers complex questions
about a database. The stored procedures in question were discussed in Chapter 16.
Remember that this is an InterBase database, and that you must set up the alias for
it as described in the readme file on the CD that accompanies this book. I have hardcoded
the password "masterkey" into the Params property of the TDatabase
object in the program's data module.
FIGURE
26.11. Running a query against the Music
database to see albums rated as PEACEFUL.
FIGURE
26.12. Running a query against the Music database to see albums
with very high ratings.
The source for the CGIQuery program is shown in Listing 26.15. A special HTML
page is needed to query this application. A screen shot of the page is shown in Figure
26.13, and the HTML source for the page is shown in Listing 26.16.
FIGURE
26.13. The HTML page used to query the
CGIQuery application.
Listing 26.15. The CGIQuery application
lets the user ask a number of questions of the database.
///////////////////////////////////////
// CGIQuery.cpp
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "DMod1.h"
#include "CGIDBHelp1.h"
USERES("CGIQuery.res");
USEDATAMODULE("DMod1.cpp", DMod);
USEUNIT("\SrcC\PUnleash\Utils\CGIDBHelp1.cpp");
USEUNIT("\SrcC\PUnleash\Utils\CGIHelp1.cpp");
AnsiString ParseQuery(AnsiString S)
{
int i = S.Pos("&");
S.SetLength(i - 1);
S = strrev(S.c_str());
i = S.Pos("=");
S.SetLength(i - 1);
S = strrev(S.c_str());
return S.c_str();
}
AnsiString ParseQuery1(AnsiString S)
{
S = strrev(S.c_str());
int i = S.Pos("=");
S.SetLength(i - 1);
return S.c_str();
}
///////////////////////////////////////
// If user pressed a button on the HTML
// form, them must parse input.
// String will look like this:
// Query=6&Query1=7
///////////////////////////////////////
AnsiString GetButtonQuery(AnsiString S)
{
AnsiString Query = ParseQuery(S);
AnsiString Query1= ParseQuery1(S);
try
{
return Format("select * from RatingRange(%s, %s)",
OPENARRAY(TVarRec, (Query, Query1)));
}
catch(...)
{
return "You entered invalid data";
}
}
bool GetLetters(AnsiString(S))
{
int i;
FILE * out;
out = fopen("c:\\sam.txt", "w+");
for (i = 0; i < S.Length(); i++)
{
fprintf(out, "Letters: %c\n", S[i]);
}
fclose(out);
return True;
}
void RunQueries()
{
int i;
TCGIDBHelp CGI;
AnsiString S = CGI.GetQuery();
if (S.Length() == 0)
S = "0";
if (S[1] != `Q') // Did user press button? If so first letter is `Q'
{
if (S != "")
i = S.ToInt();
else
i = 0;
switch(i)
{
case 0: S = "select * from Album"; break;
case 1: S = "select * from NineOrBetter"; break;
case 2: S = "select * from GetLoudness(1)"; break;
case 3: S = "select * from GetLoudness(2)"; break;
case 4: S = "select * from GetLoudness(3)"; break;
default:
S = "select * from Artist";
}
}
else
S = GetButtonQuery(S);
DMod->Query1->SQL->Add(S);
try
{
DMod->Query1->Open();
CGI.Header(DMod->Query1->SQL->Strings[0]);
CGI.PrintTable(DMod->Query1);
DMod->Query1->Close();
}
catch(...)
{
printf("Invalid Query: %s", S.c_str());
}
}
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TDMod), &DMod);
RunQueries();
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
Listing 26.16. The HTML file
used to query the CGIQuery application.
<HTML>
<TITLE>CGI QueryPage</TITLE>
<HEAD>
<H1>CGI QueryPage</H1>
</HEAD>
<body bgcolor="#0000FF" text="#00FFFF" link="#FFFF00"
vlink="#00FF00" alink="#FFFFFF">
<BODY>
<UL>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe">CGIQuery.exe</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?1">
Albums rated nine or better</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?2">Peaceful Albums</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?3">Moderate Albums</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?4">Loud Albums</a>
</UL>
<HR>
<FORM METHOD="GET" ACTION="/scripts/books/bunleash/CGIQuery.exe">
Range Start<BR>
<INPUT TYPE = "text" NAME="Query" SIZE=10 defaultvalue="1"><BR><BR>
Range End<BR>
<INPUT TYPE = "text" NAME="Query1" SIZE=10 defaultvalue="2"><BR><BR>
<INPUT TYPE = "submit" VALUE="Submit">
<HR>
</BODY>
</HTML>
The most interesting thing about the CGIQuery application is that it
uses a TDataModule, as shown in Figure 26.14. The data module allows you
to access all the visual database tools that can make BCB programming so simple.
For example, you can drop down a TDataBase object, connect to a database,
set up your password in the Params property, and open up the database. Performing
these tasks visually is much easier than doing them in code. You can also set up
a one-to-many relationship or perform filters and lookups.
FIGURE
26.14. The data module for the CGIQuery
application.
Using a data module in a CGI application allows you to leverage the power of the
BCB RAD environment.
When you're using a data module, giving up altogether on the idea of using a Console
application is probably simplest. Instead, keep the standard WinMain block
set up by BCB:
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TDMod), &DMod);
RunQueries();
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
This code calls Application::Initialize, CreateForm, and Run.
As you can see, I do not include a standard TForm object in the application,
which means that it has no visual interface. It just runs silently in the background,
without ever showing its face to the user. Once again, notice that I call Run,
which appears to be necessary when you include a data module and exception handling
in your application.
I should perhaps add that during application development, I often include a form
so that I can test my queries at design time or sometimes even at runtime. The form
usually need not contain anything but a TDBGrid in which to display the
results of the query. I include a TDataSource on the data module for use
with a form during program testing. It will be deleted during the final phases of
project development. Furthermore, you can simply remove the entire test form from
your project when it's time to distribute your application.
The key point to emphasize here is that you can leverage the visual tools during
CGI development, even if you end up stripping them out of the application when you
ship. This capability helps show why BCB is the best environment to use even when
you're not producing standard RAD applications.
NOTE: I once again want to stress that
you can design CGI applications in a number of ways. I mean for the techniques I
show here to be an introduction to the subject and to show you how to get a lot of
work done quickly. If you find that this subject interests you, you should consider
turning to third parties that have developed custom VCL components to make this task
simpler and more efficient.
Using a Browser to Ask Questions
To understand the CGI Query application, you should first look at the HTML code
that calls it:
<LI><a href="/scripts/books/bunleash/CgiQuery.exe">CGIQuery.exe</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?1">
Albums rated nine or better</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?2">Peaceful Albums</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?3">Moderate Albums</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?4">Loud Albums</a>
These options pass in various query strings, depending on the user's interests.
For example, if the query string "1" is passed in, the following
code gets called:
case 1: S = "select * from NineOrBetter"; break;
This line sets up a SQL statement that calls the stored procedure called NineOrBetter.
The code for the stored procedure looks like this:
begin
for
select Artist.Last, Album.Album, Album.Rating
from Album, Artist
where Album.GroupCode = Artist.Code and Album.Rating >= 9
Order By Album.Rating Desc
into :Last, :Album, :Rating
do
suspend;
end
As I said earlier, you can turn to Chapter 16 for an in-depth explanation of this
code. The key point is that it retrieves a list of albums rated 9 or better.
The code that runs the query looks like this:
DMod->Query1->SQL->Add(S);
try
{
DMod->Query1->Open();
CGI.Header(DMod->Query1->SQL->Strings[0]);
CGI.PrintTable(DMod->Query1);
DMod->Query1->Close();
}
catch(...)
{
printf("Invalid Query: %s", S.c_str());
}
The first line here sets up the query string. The TQuery object is then
run, and the data from it is processed by the TCGIDBHelp object, as described
in the examination of the CGIData2 application.
Besides calling the NineOrBetter stored procedure, the user's other choices
include selecting a query that retrieves data about the relative volume of the albums
in the database:
case 2: S = "select * from GetLoudness(1)"; break;
case 3: S = "select * from GetLoudness(2)"; break;
case 4: S = "select * from GetLoudness(3)"; break;
The GetLoudness stored procedure looks like this:
begin
for
select Artist.Last, Album.Album, Album.Rating, Loudness.Loudness
from Album, Artist, Loudness
where Album.GroupCode = Artist.Code and
Album.Loudness = :LoudnessValue and
Loudness.Code = :LoudnessValue
Order By Album.Album Desc
into :Last, :Album, :Rating, :LoudnessStr
do
suspend;
end
This SQL code depends on the values from the Loudness table. The Loudness
table contains three records, stating that Peaceful records have a code
of 1, Moderate records have a code of 2, and Loud
records have a code of 3. The stored procedure does a lookup into this table,
retrieves the string specifying the loudness of a particular album, and displays
it to the user.
Now that you understand how the system works, you can see that calling GetLoudness(1)
retrieves a list of the Peaceful records, GetLoudness(2) gets the
records of Moderate volume, and so on. All this information is then wrapped
up in an HTML table and shown to the user, as shown in Figure 26.11.
Submit Buttons and CGI Applications
The final point I want to make about this program is that it allows you to run
queries based on data typed in by the user, as shown Figure 26.14. In particular,
the user can type in a starting and ending range for the ratings of albums. If he
or she then clicks the Submit button, a query will be formulated based on the user's
input.
The standard HTML code that allows the user to submit the question looks like
this:
<HR>
<FORM METHOD="GET" ACTION="/scripts/books/bunleash/CGIQuery.exe">
Range Start<BR>
<INPUT TYPE = "text" NAME="Query" SIZE=10 defaultvalue="1"><BR><BR>
Range End<BR>
<INPUT TYPE = "text" NAME="Query1" SIZE=10 defaultvalue="2"><BR><BR>
<INPUT TYPE = "submit" VALUE="Submit">
<HR>
The FORM METHOD statement tells the browser what to do when the user
clicks the Submit button. In particular, it says that the CGIQuery application
should be called. The HTML then defines two edit controls and the Submit button itself.
When the user clicks the button, a string like the following is automatically
generated and passed to the application after the question mark in the URL:
Query=6&Query1=7
This string specifies that the user wants to see records that have a rating between
6 and 7.
I write some very ugly code that checks to see if the user is passing in a string
to query the ratings of a table:
AnsiString S = CGI.GetQuery();
if (S.Length() == 0)
S = "0";
if (S[1] != `Q') // Did user press button? If so first letter is `Q'
{
... // Handle queries against Loudness table, etc.
}
else
S = GetButtonQuery(S);
As you can see, I've actually commented my code in this case, because there is
no logical way for the reader to figure out why I am checking for the letter Q.
Once you understand the system, you can see that the check is made because all but
one type of query from the browser will consist of a simple integer. That one exceptional
query is the kind that asks for record albums and CDs in a particular range, and
I can identify that type of query because it always begins with a Q. This
is, as I readily confess, horrible code, even by my standards. However, in this case
I ask for your indulgence because of the rather primitive nature of all CGI technology.
NOTE: When I say that CGI technology is
primitive, I am, of course, aware that network-based technology in and of itself
is quite remarkable. However, the interface between a browser and an application
is still very rudimentary, and will probably continue to be so in the foreseeable
future. Because of the shortcomings of this technology, I take up DCOM in the next
chapter. Of course, DCOM also has its limitations. In fact, sometimes DCOM shines,
and sometimes CGI and ISAPI shine. The key point is knowing all the available technologies
so that you can choose the best ones for a particular situation.
That's all I'm going to say about the CGIQuery program. You might want to examine
parts of the application on your own. For example, you should take a look at the
code that handles exceptions and make sure you understand what I'm doing when I parse
the query string received from the browser.
I should perhaps add that various available technologies can relieve you of the
task of parsing query strings and of retrieving strings from the environment. Much
of the time, these tasks are so simple that you don't need complex code to get the
job done. However, if you need to create really big, complex CGI applications, then
you should get on the Web and see whether you can examine your available options
for offloading some of these chores.
Summary
In this chapter, you learned about ISAPI and CGI. These technologies allow you
to enhance a server with custom C++ code. In particular, they both allow you to access
databases over the World Wide Web.
You saw that ISAPI is a DLL-based technology that runs inside the address space
of the server. As a result, it is very highly optimized from a performance standpoint.
CGI technology, on the other hand, is based on executables rather than DLLs. CGI
applications, therefore, are usually slower than ISAPI applications. They are, on
the other hand, much simpler to write, or more specifically, simpler to debug.
The key point to grasp about these technologies is that they can be used to make
Web sites interactive. Though its performance is a bit slow, the Web can be a very
powerful means of reaching a large audience that can extend across the entire globe.
Many of the most exciting Web sites are powered by CGI or ISAPI, and BCB enables
you to easily participate in this process.
|